/**
 * Copyright 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule ReactUpdateQueue
 */

'use strict'

var ReactCurrentOwner = require('./ReactCurrentOwner')
var ReactInstanceMap = require('./ReactInstanceMap')
var ReactUpdates = require('./ReactUpdates')

var invariant = require('fbjs/lib/invariant')
var warning = require('fbjs/lib/warning')

function enqueueUpdate(internalInstance) {
  ReactUpdates.enqueueUpdate(internalInstance)
}

function formatUnexpectedArgument(arg) {
  var type = typeof arg
  if (type !== 'object') {
    return type
  }
  var displayName = (arg.constructor && arg.constructor.name) || type
  var keys = Object.keys(arg)
  if (keys.length > 0 && keys.length < 20) {
    return displayName + ' (keys: ' + keys.join(', ') + ')'
  }
  return displayName
}

function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
  var internalInstance = ReactInstanceMap.get(publicInstance)
  if (!internalInstance) {
    if (process.env.NODE_ENV !== 'production') {
      // Only warn when we have a callerName. Otherwise we should be silent.
      // We're probably calling from enqueueCallback. We don't want to warn
      // there because we already warned for the corresponding lifecycle method.
      process.env.NODE_ENV !== 'production'
        ? warning(
            !callerName,
            '%s(...): Can only update a mounted or mounting component. ' +
              'This usually means you called %s() on an unmounted component. ' +
              'This is a no-op. Please check the code for the %s component.',
            callerName,
            callerName,
            publicInstance.constructor.displayName
          )
        : void 0
    }
    return null
  }

  if (process.env.NODE_ENV !== 'production') {
    process.env.NODE_ENV !== 'production'
      ? warning(
          ReactCurrentOwner.current == null,
          '%s(...): Cannot update during an existing state transition (such as ' +
            "within `render` or another component's constructor). Render methods " +
            'should be a pure function of props and state; constructor ' +
            'side-effects are an anti-pattern, but can be moved to ' +
            '`componentWillMount`.',
          callerName
        )
      : void 0
  }

  return internalInstance
}

/**
 * ReactUpdateQueue allows for state updates to be scheduled into a later
 * reconciliation step.
 */
var ReactUpdateQueue = {
  /**
   * Checks whether or not this composite component is mounted.
   * @param {ReactClass} publicInstance The instance we want to test.
   * @return {boolean} True if mounted, false otherwise.
   * @protected
   * @final
   */
  isMounted: function (publicInstance) {
    if (process.env.NODE_ENV !== 'production') {
      var owner = ReactCurrentOwner.current
      if (owner !== null) {
        process.env.NODE_ENV !== 'production'
          ? warning(
              owner._warnedAboutRefsInRender,
              '%s is accessing isMounted inside its render() function. ' +
                'render() should be a pure function of props and state. It should ' +
                'never access something that requires stale data from the previous ' +
                'render, such as refs. Move this logic to componentDidMount and ' +
                'componentDidUpdate instead.',
              owner.getName() || 'A component'
            )
          : void 0
        owner._warnedAboutRefsInRender = true
      }
    }
    var internalInstance = ReactInstanceMap.get(publicInstance)
    if (internalInstance) {
      // During componentWillMount and render this will still be null but after
      // that will always render to something. At least for now. So we can use
      // this hack.
      return !!internalInstance._renderedComponent
    } else {
      return false
    }
  },

  /**
   * Enqueue a callback that will be executed after all the pending updates
   * have processed.
   *
   * @param {ReactClass} publicInstance The instance to use as `this` context.
   * @param {?function} callback Called after state is updated.
   * @param {string} callerName Name of the calling function in the public API.
   * @internal
   */
  enqueueCallback: function (publicInstance, callback, callerName) {
    ReactUpdateQueue.validateCallback(callback, callerName)
    var internalInstance = getInternalInstanceReadyForUpdate(publicInstance)

    // Previously we would throw an error if we didn't have an internal
    // instance. Since we want to make it a no-op instead, we mirror the same
    // behavior we have in other enqueue* methods.
    // We also need to ignore callbacks in componentWillMount. See
    // enqueueUpdates.
    if (!internalInstance) {
      return null
    }

    if (internalInstance._pendingCallbacks) {
      internalInstance._pendingCallbacks.push(callback)
    } else {
      internalInstance._pendingCallbacks = [callback]
    }
    // TODO: The callback here is ignored when setState is called from
    // componentWillMount. Either fix it or disallow doing so completely in
    // favor of getInitialState. Alternatively, we can disallow
    // componentWillMount during server-side rendering.
    enqueueUpdate(internalInstance)
  },

  enqueueCallbackInternal: function (internalInstance, callback) {
    if (internalInstance._pendingCallbacks) {
      internalInstance._pendingCallbacks.push(callback)
    } else {
      internalInstance._pendingCallbacks = [callback]
    }
    enqueueUpdate(internalInstance)
  },

  /**
   * Forces an update. This should only be invoked when it is known with
   * certainty that we are **not** in a DOM transaction.
   *
   * You may want to call this when you know that some deeper aspect of the
   * component's state has changed but `setState` was not called.
   *
   * This will not invoke `shouldComponentUpdate`, but it will invoke
   * `componentWillUpdate` and `componentDidUpdate`.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @internal
   */
  enqueueForceUpdate: function (publicInstance) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'forceUpdate'
    )

    if (!internalInstance) {
      return
    }

    internalInstance._pendingForceUpdate = true

    enqueueUpdate(internalInstance)
  },

  /**
   * Replaces all of the state. Always use this or `setState` to mutate state.
   * You should treat `this.state` as immutable.
   *
   * There is no guarantee that `this.state` will be immediately updated, so
   * accessing `this.state` after calling this method may return the old value.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {object} completeState Next state.
   * @internal
   */
  enqueueReplaceState: function (publicInstance, completeState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'replaceState'
    )

    if (!internalInstance) {
      return
    }

    internalInstance._pendingStateQueue = [completeState]
    internalInstance._pendingReplaceState = true

    enqueueUpdate(internalInstance)
  },

  /**
   * Sets a subset of the state. This only exists because _pendingState is
   * internal. This provides a merging strategy that is not available to deep
   * properties which is confusing. TODO: Expose pendingState or don't use it
   * during the merge.
   *
   * @param {ReactClass} publicInstance The instance that should rerender.
   * @param {object} partialState Next partial state to be merged with state.
   * @internal
   */
  enqueueSetState: function (publicInstance, partialState) {
    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState'
    )

    if (!internalInstance) {
      return
    }

    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = [])
    queue.push(partialState)

    enqueueUpdate(internalInstance)
  },

  enqueueElementInternal: function (internalInstance, newElement) {
    internalInstance._pendingElement = newElement
    enqueueUpdate(internalInstance)
  },

  validateCallback: function (callback, callerName) {
    !(!callback || typeof callback === 'function')
      ? process.env.NODE_ENV !== 'production'
        ? invariant(
            false,
            '%s(...): Expected the last optional `callback` argument to be a ' +
              'function. Instead received: %s.',
            callerName,
            formatUnexpectedArgument(callback)
          )
        : invariant(false)
      : void 0
  }
}

module.exports = ReactUpdateQueue
